package trim.pl.classloader;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import trim.Trim;

/**
 *
 * @author Дмитрий
 */
public class TrimClassLoader extends ClassLoader
{
  private File file=null;
  private HashMap<String,Class<?>> cache=new HashMap<String,Class<?>>();
  private String packageName="trim";
  private Map<String,byte[]> classesSource=new HashMap();
  private Map<String,byte[]> resource=new HashMap();
  private List<Trim> registeredTrimMethods=new ArrayList();

  public TrimClassLoader(String fileName)
  {
    loadPlagin(fileName);
  }
  public TrimClassLoader(File file)
  {
    loadPlagin(file);
  }

  public int getMethodCount()
  {
    return registeredTrimMethods.size();
  }

  /**
   * Загрузка плагина. По сути обычный загрузчик классов, но который
   * вылеляет методы решения ЗФР.
   * @param jarFileName
   * @return
   */
  private Trim[] loadPlagin(String jarFileName)
  {
    file=new File(jarFileName);
    return loadPlagin(file);
  }
/**
   * Загрузка плагина. По сути обычный загрузчик классов, но который
   * вылеляет методы решения ЗФР.
   * @param jarFile
   * @return
   */
  private Trim[] loadPlagin(File jarFile)
  {
    file=jarFile;
    cacheClasses(jarFile);
    return registeredTrimMethods.toArray(new Trim[0]);
  }

  /**
   * При создании загрузчика извлекаем все классы из jar и кэшируем в памяти
   *
   */
  private void cacheClasses(File file)
  {
    try{
      JarFile jarFile=new JarFile(file);

      Enumeration entries=jarFile.entries();

      while(entries.hasMoreElements()){
        JarEntry jarEntry=(JarEntry)entries.nextElement();
        // Одно из назначений хорошего загрузчика - валидация классов на этапе загрузки
        if(!jarEntry.isDirectory()&&match(normalize(jarEntry.getName()),packageName)){
          byte[] classData=loadClassData(jarFile,jarEntry);
          if(jarEntry.getName().endsWith(".class"))
            classesSource.put(stripClassName(normalize(jarEntry.getName())),classData);
          else
            resource.put(jarEntry.getName(),classData);
        }
      }
      List<String> l=new ArrayList();
      for(String s:classesSource.keySet())
        l.add(s);
      for(String s:l){
        try{
          System.out.println("Loding class "+s);
          Class clazz=loadClass(s);
          if(clazz!=null&&Trim.class.isAssignableFrom(clazz)){
            System.out.println("Registering trim method "+clazz.getCanonicalName());
            try{
              Trim trim=(Trim)clazz.newInstance();
              registeredTrimMethods.add(trim);
            }catch(Exception e){
              System.out.println("Unable instantiate class "+clazz.getCanonicalName()+". May be it abstract.");
            }
          }
        }catch(Exception ex){
          ex.printStackTrace();
        }
      }
      l=null;
      classesSource.clear();
    }catch(IOException ex){
      ex.printStackTrace();
    }
    Collections.sort(registeredTrimMethods,new Comparator<Trim>() {

      public int compare(Trim o1,Trim o2)
      {
        return o1.getName().compareTo(o2.getName());
      }
    });
  }

  /**
   * Собственно метод, который и реализует загрузку класса
   *
   */
  @Override
  public synchronized Class<?> loadClass(String name) throws ClassNotFoundException
  {

    Class<?> result=cache.get(name);
    // Возможно класс вызывается не по полному имени - добавим имя пакета
    if(result==null)
      result=cache.get(packageName+"."+name);
    if(result==null){
      byte[] classData=classesSource.get(name);
      if(classData!=null){
        Class<?> clazz=defineClass(name,classData,0,classData.length);
        cache.put(clazz.getName(),clazz);
        classesSource.remove(name);
        result=clazz;
      }
    }
    // Если класса нет в кэше то возможно он системный
    if(result==null)
      result=super.findSystemClass(name);
    //System.out.println("== loadClass("+name+")");

    return result;
  }

  /**
   * Получаем каноническое имя класса
   * @param className
   * @return
   */
  private String stripClassName(String className)
  {
    return className.substring(0,className.length()-6);
  }

  /**
   * Преобразуем имя в файловой системе в имя класса
   * (заменяем слэши на точки)
   *
   * @param className
   * @return
   */
  private String normalize(String className)
  {
    return className.replace('/','.');
  }

  /**
   * Валидация класса - проверят принадлежит ли класс заданному пакету и имеет ли
   * он расширение .class
   *
   * @param className
   * @param packageName
   * @return
   */
  private boolean match(String className,String packageName)
  {
    return className.startsWith(packageName);//&&className.endsWith(".class");
  }

  /**
   * Извлекаем файл из заданного JarEntry
   *
   * @param jarFile - файл jar-архива из которого извлекаем нужный файл
   * @param jarEntry - jar-сущность которую извлекаем
   * @return null если невозможно прочесть файл
   */
  private byte[] loadClassData(JarFile jarFile,JarEntry jarEntry) throws IOException
  {
    long size=jarEntry.getSize();
    if(size==-1||size==0)
      return null;
    byte[] data=new byte[(int)size];
    InputStream in=jarFile.getInputStream(jarEntry);
    in.read(data);

    return data;

  }

  @Override
  public InputStream getResourceAsStream(String name)
  {
    InputStream is=super.getResourceAsStream(name);
    if(is==null){
      byte[] a=resource.get(name);
      if(a!=null)
        return new ByteArrayInputStream(a);
    }
    return null;
  }

  public List<Trim> getRegisteredTrimMethods()
  {
    return registeredTrimMethods;
  }

  public Trim getTrimMethod(String className)
  {
    for(Trim t:registeredTrimMethods)
      if(t.getClass().getName().equals(className))
        return t;
    return null;
  }
/**
 * Плагин
 * @return плагин
 */
  public File getFile()
  {
    return file;
  }
  
/**
 * Имя файла, содержащего плагин
 * @return
 */
  public String getPlaginName()
  {
    if(file==null)
      return null;
    return file.getName();
  }
}